import { App, computed, defineComponent, ExtractPropTypes, normalizeClass, normalizeStyle, onMounted, onUnmounted, PropType, ref, StyleValue, toRef, watchPostEffect } from 'vue'
import { MaybeRef } from '@vueuse/core'
import { useMergedState } from '../_util/hooks'
import useDraggable from '../_util/Dom/useDraggable'

type STICKS = 'tl' | 'tc' | 'tr' | 'cl' | 'cr' | 'bl' | 'bc' | 'br' | ''

const sticks: Readonly<STICKS[]> = Object.freeze(['tl', 'tc', 'tr', 'cl', 'cr', 'bl', 'bc', 'br'])

const prefixCls = 'x-drag-resize'

export const resizableProps = {
  sticks: Array as PropType<STICKS[]>,
  x: Number,
  y: Number,
  w: Number,
  h: Number,
  initW: Number,
  initH: Number,
  minW: { type: Number, default: 20 },
  minH: { type: Number, default: 20 },
  maxW: { type: Number, default: undefined },
  maxH: { type: Number, default: undefined },
  active: { type: Boolean, default: undefined },
  type: { type: String as PropType<'default' | 'simple'>, default: 'default' },
  resizingClass: [String, Object, Array],
  resizableClass: [String, Object, Array],
  activeClass: [String, Object, Array],
  activeStyle: [String, Object, Array] as PropType<StyleValue>,
  stickClass: [String, Object, Array],
  stickStyle: [String, Object, Array] as PropType<StyleValue>,
  aspectRatio: Boolean,
  resizable: { type: Boolean, default: true },
  draggable: { type: Boolean, default: true },
  dragTarget: Object as PropType<MaybeRef<HTMLElement>>,
}

export type ResizableProps = ExtractPropTypes<typeof resizableProps>

const Resizable = defineComponent({
  name: prefixCls,
  props: resizableProps,
  emits: [
    'update:x', 'update:y', 'update:w', 'update:h',
    'update:active',
    'resizing', 'resize-start', 'resize-end',
    'activated', 'deactivated',
  ],
  setup(props, { slots, emit }) {
    const resizingRef = ref(false)

    const xref = createBinding('x'), wref = createBinding('w', props.initW)
    const yref = createBinding('y'), href = createBinding('h', props.initH)
    
    const clazzRef = computed(() => normalizeClass([
      prefixCls,
      `${prefixCls}-${props.type}`,
      resizingRef.value && props.resizingClass,
      props.resizable && props.resizableClass,
      activeRef.value && [`${prefixCls}-active`, props.activeClass]
    ]))

    const styleRef = computed(() => {
      let style: StyleValue = {
        left: xref.value + 'px',
        top: yref.value + 'px',
        width: wref.value + 'px',
        height: href.value + 'px',
      }
      if (activeRef.value && props.activeStyle) {
        style = normalizeStyle([props.activeStyle, style])
      }
      return style
    })

    
    function getPosition(e: MouseEvent | TouchEvent) {
      if ('touches' in e) {
        return [e.touches[0].pageX, e.touches[0].pageY]
      } else {
        return [e.pageX, e.pageY]
      }
    }

    function ensureInRange(num: number, min: number, max: number) {
      return Math.max(min, Math.min(max, num))
    }

    function createBinding<K extends keyof ResizableProps>(prop: K, initV?: ResizableProps[K]) {
      // @ts-ignore
      const mergeRef = useMergedState<ResizableProps[K]>(toRef(props, prop), ref(initV), () => emit(`update:${prop}`))
      return mergeRef
    }


    // ========================== resizing ==========================
    const elRef = ref<HTMLElement>()
    let startPageX = 0, startPageY = 0, startX = 0, startY = 0, startW = 0, startH = 0
    let stick: STICKS = ''

    let transformOrigin: [number, number]
    let transformOriginMap: { [key in STICKS]?: (x, y, w, h) => [number, number] } = {
      tl: (x, y, w, h) => [w, h],
      tc: (x, y, w, h) => [w / 2, h],
      tr: (x, y, w, h) => [0, h],
      cl: (x, y, w, h) => [w, h / 2],
      cr: (x, y, w, h) => [0, h / 2],
      bl: (x, y, w, h) => [w, 0],
      bc: (x, y, w, h) => [w / 2, 0],
      br: (x, y, w, h) => [0, 0],
    }
    
    function onstickDown(e: MouseEvent | TouchEvent, stickType: STICKS) {
      if (!props.resizable) return
      e.preventDefault()
      e.stopPropagation()
      resizingRef.value = true
      ;[startPageX, startPageY] = getPosition(e)
      startX = parseInt(elRef.value.style.left) || 0
      startY = parseInt(elRef.value.style.top) || 0
      startW = elRef.value.offsetWidth
      startH = elRef.value.offsetHeight
      transformOrigin = transformOriginMap[stickType](startX, startY, startW, startH)
      stick = stickType
      window.addEventListener('mousemove', onMousemove)
      window.addEventListener('mouseup', onMouseup)
      emit('resize-start')
    }
    function onMousemove(e: MouseEvent) {
      e.preventDefault()
      const [x, y] = getPosition(e)
      let deltaX = x - startPageX, deltaY = y - startPageY

      const maxW = props.maxW ?? Infinity, minW = Math.max(props.minW, 0)

      const setW = (rate: number, syncH?: boolean) => {
        rate = Math.max(rate, 0)
        let w = ensureInRange(startW * rate, minW, maxW)
        const minx = startX + (startW - maxW) * (transformOrigin[0] / startW)
        const maxx = startX + (startW - minW) * (transformOrigin[0] / startW)
        const x = ensureInRange(startX + (transformOrigin[0] * (1 - rate)), minx, maxx)
        xref.value = x
        wref.value = w
        if (syncH) setH(rate)
      }

      const maxH = props.maxH ?? Infinity, minH = Math.max(props.minH, 0)

      const setH = (rate: number, syncW?: boolean) => {
        rate = Math.max(rate, 0)
        let h = ensureInRange(startH * rate, minH, maxH)
        const miny = startY + (startH - maxH) * (transformOrigin[1] / startH)
        const maxy = startY + (startH - minH) * (transformOrigin[1] / startH)
        const y = ensureInRange(startY + (transformOrigin[1] * (1 - rate)), miny, maxy)
        yref.value = y
        href.value = h
        if (syncW) setW(rate)
      }
      
      if (stick.includes('l') || stick.includes('r')) {
        deltaX *= stick.includes('l') ? -1 : 1
        let distance = stick.includes('l') ? transformOrigin[0] : startW - transformOrigin[0]
        let rate = (distance + deltaX) / distance
        setW(rate, props.aspectRatio)
      }
      
      if (stick.includes('t') || stick.includes('b')) {
        deltaY *= stick.includes('t') ? -1 : 1
        let distance = stick.includes('t') ? transformOrigin[1] : startH - transformOrigin[1]
        let rate = (distance + deltaY) / distance
        setH(rate, props.aspectRatio)
      }
      emit('resizing')
    }
    function onMouseup() {
      window.removeEventListener('mousemove', onMousemove)
      window.removeEventListener('mouseup', onMouseup)
      emit('resize-end')
    }


    // ========================== draggable ==========================
    const { enable } = useDraggable(elRef, { dragTarget: props.dragTarget, x: xref, y: yref })
    onMounted(() => {
      watchPostEffect(() => enable(props.draggable))
    })
    onUnmounted(() => {
      enable(false)
    })


    // ========================== active ==========================
    const activeRef = useMergedState(toRef(props, 'active'), ref(false), setActive)

    function setActive(val: boolean) {
      emit(val ? 'activated' : 'deactivated')
      emit('update:active', val)
    }
    function onActive() {
      if (activeRef.value) return
      activeRef.value = true
    }
    function onDeactivated(e: MouseEvent) {
      if (!activeRef.value) return
      if (elRef.value.contains(e.target as Node)) return
      activeRef.value = false
    }
    onMounted(() => {
      document.documentElement.addEventListener('mousedown', onDeactivated)
    })
    onUnmounted(() => {
      document.documentElement.removeEventListener('mousedown', onDeactivated)
    })


    // ========================== Render ==========================
    const stickNodes = computed(() => (props.resizable ? (props.sticks || sticks) : []).map(item => 
      <div
        class={`${prefixCls}_stick ${prefixCls}_stick-${item} ${props.stickClass || ''} ${props.stickClass || ''}-${item}`}
        style={props.stickStyle}
        onMousedown={e => onstickDown(e, item)}
        onTouchstart={e => onstickDown(e, item)}
      />))

    return () => (
      <div class={clazzRef.value} style={styleRef.value} ref={elRef} onMousedown={onActive}>
        {slots.default?.()}
        {stickNodes.value}
      </div>
    )
  }
})

Resizable.install = (app: App) => {
  app.component(Resizable.name, Resizable)
}

export default Resizable